package cn.fraudmetrix.octopus.modules.jacoco.service;

import cn.fraudmetrix.octopus.common.utils.R;
import cn.fraudmetrix.octopus.modules.jacoco.constant.JacocoConstant;
import cn.fraudmetrix.octopus.modules.jacoco.dao.JacocoCoverageDao;
import cn.fraudmetrix.octopus.modules.jacoco.entity.JacocoEntity;
import org.apache.commons.lang.StringUtils;
import org.dstadler.jgit.helper.CookbookHelper;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.jacoco.core.analysis.Analyzer;
import org.jacoco.core.analysis.CoverageBuilder;
import org.jacoco.core.analysis.IBundleCoverage;
import org.jacoco.core.tools.ExecFileLoader;
import org.jacoco.report.DirectorySourceFileLocator;
import org.jacoco.report.FileMultiReportOutput;
import org.jacoco.report.IReportVisitor;
import org.jacoco.report.MultiSourceFileLocator;
import org.jacoco.report.html.HTMLFormatter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @author libao.zheng
 * @email libao.zheng@tongdun.cn
 * @date 2018/12/5 11:26
 */
public class ReportGenerator {
    private final Logger LOGGER = LoggerFactory.getLogger(getClass());
    private File executionDataFile;
    private List<File> classesDirectory = new ArrayList<>();
    private List<File> sourceDirectory = new ArrayList<>();
    private File reportDirectory;
    private String title;
    private ExecFileLoader execFileLoader;
    private Set<String> changedMethodNames;
    private List<String> newClassNames;
    private List<String> changedClassNames;
    private boolean incremental;
    private boolean gitUpdated;

    private void create() {
        try {
            loadExecutionData();
            final IBundleCoverage bundleCoverage = analyzeStructure();
            createReport(bundleCoverage);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void createReport(final IBundleCoverage bundleCoverage)
            throws IOException {
        final HTMLFormatter htmlFormatter = new HTMLFormatter();
        final IReportVisitor visitor = htmlFormatter
                .createVisitor(new FileMultiReportOutput(this.reportDirectory));
        visitor.visitInfo(this.execFileLoader.getSessionInfoStore().getInfos(),
                this.execFileLoader.getExecutionDataStore().getContents());
        MultiSourceFileLocator multiSourceFileLocator = new MultiSourceFileLocator(4);
        if (this.sourceDirectory != null && this.sourceDirectory.size() != 0) {
            for (File file : this.sourceDirectory) {
                multiSourceFileLocator.add(new DirectorySourceFileLocator(file, StandardCharsets.UTF_8.name(), 4));
            }
        }
        visitor.visitBundle(bundleCoverage, multiSourceFileLocator);
        visitor.visitEnd();
    }

    private void loadExecutionData() throws IOException {
        this.execFileLoader = new ExecFileLoader();
        this.execFileLoader.load(this.executionDataFile);
    }

    private IBundleCoverage analyzeStructure() throws IOException {
        final CoverageBuilder coverageBuilder = new CoverageBuilder();
        final Analyzer analyzer = new Analyzer(this.execFileLoader.getExecutionDataStore(), coverageBuilder);
        analyzer.setChangedMethodNames(this.changedMethodNames);
        analyzer.setNewClassNames(this.newClassNames);
        analyzer.setChangedClassNames(this.changedClassNames);
        analyzer.setIncremental(this.incremental);
        if (this.classesDirectory != null && this.classesDirectory.size() != 0) {
            for (File file : this.classesDirectory) {
                analyzer.analyzeAll(file);
            }
        }
        return coverageBuilder.getBundle(this.title);
    }

    public R getJacocoReport(JacocoEntity jacocoAppForm, JacocoCoverageDao jacocoCoverageDao) {
        Map<String, Object> map = new HashMap<>();
        Integer appId = jacocoAppForm.getAppId();
        String appName = jacocoAppForm.getAppName();
        String profileName = jacocoAppForm.getProfileName();
        String compileCmd = jacocoAppForm.getCompileCmd();
        String sd = jacocoAppForm.getSourceDirectory();
        String cd = jacocoAppForm.getClassesDirectory();
        String appGit = jacocoAppForm.getAppGit();
        String appBranch = jacocoAppForm.getAppBranch();
        String appIp = jacocoAppForm.getAppIp();
        int appPort = jacocoAppForm.getAppPort();
        boolean clearHistory = jacocoAppForm.isClearHistory();

        //dump exec file
        try {
            ExecutionDataClient.dump(appName, appIp, appPort, clearHistory);
            LOGGER.info("appName:{} dump exec file from ip:{} port:{} success", appName, appIp, appPort);
        } catch (Exception e) {
            map.put("code", 2004);
            map.put("msg", "get dump file from agent error,请检查agent ip是否正确,端口号是否开启");
            e.printStackTrace();
            return R.ok(map);
        }

        //clone source code to local
        ShowBranchDiff showBranchDiff = new ShowBranchDiff();
        try {
            if (showBranchDiff.cloneBranch(appGit, appName, appBranch)) {
                this.setGitUpdated(true);
            } else {
                this.setGitUpdated(false);
            }
            showBranchDiff.cloneBranch(appGit, appName, JacocoConstant.MASTER_BRANCH);
            LOGGER.info("appName:{},appBranch:{} and {} git clone success", appName, appBranch, JacocoConstant.MASTER_BRANCH);
        } catch (Exception e) {
            map.put("code", 2001);
            map.put("msg", "git clone error,请检查git地址和git分支是否正确");
            return R.ok(map);
        }

        //get diff class
        try {
            Repository repositoryNew = CookbookHelper.openJGitCookbookRepository(JacocoConstant.LOCAL_RESOURCE + appName + "/" + appBranch + "/", JacocoConstant.GIT_SUFFIX);
            Repository repositoryOld = CookbookHelper.openJGitCookbookRepository(JacocoConstant.LOCAL_RESOURCE + appName + "/" + JacocoConstant.MASTER_BRANCH + "/", JacocoConstant.GIT_SUFFIX);
            Git git = new Git(repositoryNew);
            AbstractTreeIterator newTreeParser = showBranchDiff.prepareTreeParser(repositoryNew, JacocoConstant.REF_NAME + appBranch);
            AbstractTreeIterator oldTreeParser = showBranchDiff.prepareTreeParser(repositoryOld, JacocoConstant.REF_NAME + JacocoConstant.MASTER_BRANCH);
            showBranchDiff.getDiffClass(appName, git, newTreeParser, oldTreeParser);
            LOGGER.info("appName:{},appBranch:{} and {} get diff class success", appName, appBranch, JacocoConstant.MASTER_BRANCH);
        } catch (Exception e) {
            map.put("code", 2002);
            map.put("msg", "get diff class error,请检查git地址和git分支是否正确");
            return R.ok(map);
        }
        for (String item : showBranchDiff.getModifiedJava()) {
            try {
                showBranchDiff.getDiffMethod(appName, appBranch, item);
            } catch (Exception e) {
                map.put("code", 2003);
                map.put("msg", "get diff method error,请检查文件路径是否正确");
                return R.ok(map);
            }
        }
        LOGGER.info("appName:{},appBranch:{} get diff method success", appName, appBranch);

        //get diff method
        for (String item : showBranchDiff.getModifiedJava()) {
            try {
                showBranchDiff.getDiffMethod(appName, JacocoConstant.MASTER_BRANCH, item);
            } catch (Exception e) {
                map.put("code", 2003);
                map.put("msg", "get diff method error,请检查文件路径是否正确");
                return R.ok(map);
            }
        }
        LOGGER.info("appName:{},appBranch:{} get diff method success", appName, JacocoConstant.MASTER_BRANCH);
        //get modified methods
        for (Map.Entry<String, Integer> entry : showBranchDiff.getNewMethodInfo().entrySet()) {
            String key = entry.getKey();
            Integer value = entry.getValue();
            Integer result = showBranchDiff.getOldMethodInfo().get(key);
            if (result == null) {
                showBranchDiff.getModifiedMethod().add(key.substring(key.indexOf("src/main/java/") + 14));
            } else {
                if (!value.equals(result)) {
                    showBranchDiff.getModifiedMethod().add(key.substring(key.indexOf("src/main/java/") + 14));
                }
            }
        }

        //generator profile
        File branchNewBaseDir = new File(JacocoConstant.LOCAL_RESOURCE + appName + "/" + appBranch);
        if (StringUtils.isNotBlank(profileName)) {
            try {
                Shell.execCmd("touch " + profileName, branchNewBaseDir);
                LOGGER.info("appName:{},appBranch:{} generator profile success", appName, appBranch);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        //build class file
        try {
            if (this.gitUpdated) {
                String s = Shell.execCmd(compileCmd, branchNewBaseDir);
                LOGGER.info(s);
                LOGGER.info("appName:{},appBranch:{} build class file success", appName, appBranch);
            } else {
                LOGGER.warn("appName:{},appBranch:{} not modified,build class file is ignored", appName, appBranch);
            }
        } catch (Exception e) {
            map.put("code", 2005);
            map.put("msg", "build class error,请检查编译命令和配置文件是否正确");
            return R.ok(map);
        }

        //generator report
        if (sd.contains(JacocoConstant.SEPARATOR_COMMA)) {
            for (String item : sd.split(JacocoConstant.SEPARATOR_COMMA))
                this.sourceDirectory.add(new File(branchNewBaseDir + "/" + item));
        } else {
            this.sourceDirectory.add(new File(branchNewBaseDir + "/" + sd));
        }

        if (cd.contains(JacocoConstant.SEPARATOR_COMMA)) {
            for (String item : cd.split(JacocoConstant.SEPARATOR_COMMA))
                this.classesDirectory.add(new File(branchNewBaseDir + "/" + item));
        } else {
            this.classesDirectory.add(new File(branchNewBaseDir + "/" + cd));
        }

        this.setIncremental(jacocoAppForm.isIncremental());
        if (this.incremental) {
            String appIncrementalPath = "coverage/incremental/" + appName;
            try {
                Shell.execCmd("rm -rf " + appIncrementalPath, new File(JacocoConstant.LOCAL_RESOURCE));
                Shell.execCmd("rm -rf " + appIncrementalPath, new File(JacocoConstant.LOCAL_RESOURCE + JacocoConstant.CLASSES_PATH));
                LOGGER.info("appName:{} branchName:{} history incremental report clear success", appName, appBranch);
            } catch (IOException e) {
                e.printStackTrace();
            }
            this.setReportDirectory(new File(JacocoConstant.LOCAL_RESOURCE + appIncrementalPath));
        } else {
            String appFullPath = "coverage/full/" + appName;
            try {
                Shell.execCmd("rm -rf " + appFullPath, new File(JacocoConstant.LOCAL_RESOURCE));
                Shell.execCmd("rm -rf " + appFullPath, new File(JacocoConstant.LOCAL_RESOURCE + JacocoConstant.CLASSES_PATH));
                LOGGER.info("appName:{} branchName:{} history full report clear success", appName, appBranch);
            } catch (IOException e) {
                e.printStackTrace();
            }
            this.setReportDirectory(new File(JacocoConstant.LOCAL_RESOURCE + appFullPath));
        }
        this.setExecutionDataFile(new File(JacocoConstant.LOCAL_RESOURCE + appName + "/jacoco.exec"));
        this.setNewClassNames(showBranchDiff.getNewClass());
        this.setChangedClassNames(showBranchDiff.getModifiedClass());
        this.setChangedMethodNames(showBranchDiff.getModifiedMethod());
        this.create();
        LOGGER.info("appName:{} branchName:{} generator report success", appName, appBranch);

        //calculate and store coverage
        try {
            JacocoCoverageService.calculate(appId, appName, this.incremental, new File(this.getReportDirectory().getAbsolutePath() + "/index.html"), jacocoCoverageDao);
            LOGGER.info("appName:{} branchName:{} calculate and store coverage success", appName, appBranch);
        } catch (IOException e) {
            e.printStackTrace();
        }

        //copy coverage file
        try {
            Shell.execCmd(JacocoConstant.COPY_CMD, new File(JacocoConstant.LOCAL_RESOURCE));
            LOGGER.info("appName:{} branchName:{} {}", appName, appBranch, JacocoConstant.COPY_CMD);
            map.put("code", 0);
            if (this.incremental) {
                map.put("msg", "增量覆盖率报告生成成功");
            } else {
                map.put("msg", "全量覆盖率报告生成成功");
            }
        } catch (Exception e) {
            map.put("code", 2006);
            map.put("msg", "copy file error,请检查copy命令是否正确");
            return R.ok(map);
        }
        return R.ok(map);
    }


    public File getExecutionDataFile() {
        return executionDataFile;
    }

    public void setExecutionDataFile(File executionDataFile) {
        this.executionDataFile = executionDataFile;
    }

    public List<File> getClassesDirectory() {
        return classesDirectory;
    }

    public void setClassesDirectory(List<File> classesDirectory) {
        this.classesDirectory = classesDirectory;
    }

    public List<File> getSourceDirectory() {
        return sourceDirectory;
    }

    public void setSourceDirectory(List<File> sourceDirectory) {
        this.sourceDirectory = sourceDirectory;
    }

    public File getReportDirectory() {
        return reportDirectory;
    }

    public void setReportDirectory(File reportDirectory) {
        this.reportDirectory = reportDirectory;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public ExecFileLoader getExecFileLoader() {
        return execFileLoader;
    }

    public void setExecFileLoader(ExecFileLoader execFileLoader) {
        this.execFileLoader = execFileLoader;
    }

    public Set<String> getChangedMethodNames() {
        return changedMethodNames;
    }

    public void setChangedMethodNames(Set<String> changedMethodNames) {
        this.changedMethodNames = changedMethodNames;
    }

    public List<String> getNewClassNames() {
        return newClassNames;
    }

    public void setNewClassNames(List<String> newClassNames) {
        this.newClassNames = newClassNames;
    }

    public List<String> getChangedClassNames() {
        return changedClassNames;
    }

    public void setChangedClassNames(List<String> changedClassNames) {
        this.changedClassNames = changedClassNames;
    }

    public boolean isIncremental() {
        return incremental;
    }

    public void setIncremental(boolean incremental) {
        this.incremental = incremental;
    }

    public boolean isGitUpdated() {
        return gitUpdated;
    }

    public void setGitUpdated(boolean gitUpdated) {
        this.gitUpdated = gitUpdated;
    }
}
